﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright company="Microsoft Corporation" file="TranslatorTools.cs">
//   Copyright © Microsoft Corporation.  All Rights Reserved.  
// This code released under the terms of the 
// Microsoft Public License (MS-PL, http://opensource.org/licenses/ms-pl.html.)
// </copyright>
// <summary>
//   Resx translator common tools.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Microsoft.MCSUK.RESXTranslator
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Resources;
    using System.ServiceModel;
    using System.ServiceModel.Channels;
    using System.Web.Services.Protocols;
    using System.Windows.Forms;

    using Microsoft.MCSUK.RESXTranslator.Properties;

    /// <summary>
    /// Helper class for the ResX translator
    /// </summary>
    public static class TranslatorTools
    {
        /// <summary>
        /// Web address of landing page for Azure market for Microsoft Translator
        /// </summary>
        public const string AzureMarketTranslatorLandingPage =
            "https://datamarket.azure.com/dataset/1899a118-d202-492c-aa16-ba21c33c06cb";

        /// <summary>
        /// resx file extension
        /// </summary>
        private const string ResxExtension = ".resx";

        /// <summary>
        /// Delegate that is used to receive a log event from the issuer of a translation request
        /// </summary>
        /// <param name="message">The message.</param>
        public delegate void LogEvent(string message);

        /// <summary>
        /// Delegate that is used to issue a abort request in a translation process by the issuer of the request
        /// </summary>
        /// <returns>Boolean value indiating whether translation shall be aborted</returns>
        public delegate bool ShallAbort();

        /// <summary>
        /// Find the matching culture from a Resx file name from a number of given cultures
        /// </summary>
        /// <param name="candidates">the cultures to choose from</param>
        /// <param name="fileName">the resx file name</param>
        /// <returns>The best matching culture</returns>
        public static CultureInfo FindMatchingCultureFromResxFileName(
            IEnumerable<CultureInfo> candidates, string fileName)
        {
            if (ResxExtension.Equals(Path.GetExtension(fileName), StringComparison.CurrentCultureIgnoreCase))
            {
                var temp = Path.GetFileNameWithoutExtension(fileName);
                var cultureExtension = Path.GetExtension(temp);
                if (!string.IsNullOrEmpty(cultureExtension))
                {
                    try
                    {
                        var resxCi = CultureInfo.GetCultureInfo(cultureExtension.Trim('.'));
                        foreach (var ci in
                            candidates.Where(ci => (ci.LCID == resxCi.LCID) || (ci.LCID == resxCi.Parent.LCID)))
                        {
                            return ci;
                        }
                    }
                    catch (ArgumentException)
                    {
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Returns the credentials string for an azure account key
        /// </summary>
        /// <param name="clientID">The service account client ID</param>
        /// <param name="clientSecret">The service account client Secret</param>
        /// <param name="logEvent">Event logging delegate</param>
        /// <returns>The servcie credientals string</returns>
        public static string RequestAuthenticationToken(string clientID, string clientSecret, LogEvent logEvent)
        {
            var admAuth = new AdmAuthentication(clientID, clientSecret);
            try
            {
                var admToken = admAuth.GetAccessToken();
                var tokenReceived = DateTime.Now;

                // Create a header with the access_token property of the returned token
                return "Bearer " + admToken.AccessToken;
            }
            catch (Exception e)
            {
                LogExceptionError(e, logEvent);
                return null;
            }
        }

        /// <summary>
        /// Requests th eids for all language supported by the service
        /// </summary>
        /// <param name="authenticationToken">The service credentials, e.g. bing app id or Azure Account key.</param>
        /// <param name="logEvent">Event logging delegate</param>
        /// <returns>Array of identifiers of supported languages</returns>
        public static string[] GetLanguagesForTranslate(string authenticationToken, LogEvent logEvent)
        {
            // Add TranslatorService as a service reference, Address:http://api.microsofttranslator.com/V2/Soap.svc
            var translator = new MicrosoftTranslatorService.LanguageServiceClient();
            var binding = (BasicHttpBinding)translator.Endpoint.Binding;
            binding.MaxReceivedMessageSize = int.MaxValue;
            binding.MaxBufferSize = int.MaxValue;

            // Set Authorization header before sending the request
            var httpRequestProperty = new HttpRequestMessageProperty { Method = "POST" };
            httpRequestProperty.Headers.Add("Authorization", authenticationToken);

            string[] languagesForTranslate;

            // Creates a block within which an OperationContext object is in scope.
            using (new OperationContextScope(translator.InnerChannel))
            {
                OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] =
                    httpRequestProperty;

                // Keep appId parameter blank as we are sending access token in authorization header.
                languagesForTranslate = translator.GetLanguagesForTranslate(string.Empty);
            }

            return languagesForTranslate;
        }

        /// <summary>Translates resx files in directory recursive</summary>
        /// <param name="searchRoot">The search root path.</param>
        /// <param name="searchFilter">The search filter.</param>        
        /// <param name="fromCultureBing">The from culture.</param>
        /// <param name="toCultureBing">The to culture.</param>
        /// <param name="toCultureFile">The Target culture for the resx file.</param>
        /// <param name="authenticationToken">The service credentials, e.g. bing app id or Azure Account key.</param>
        /// <param name="logEvent">Event logging delegate</param>
        /// <param name="abortRequested">Event for checking whether abort has been requested</param>
        /// <returns>Number of files translated</returns>
        public static TranslationResult TranslateResXDirectory(
            string searchRoot,
            string searchFilter,
            CultureInfo fromCultureBing,
            CultureInfo toCultureBing,
            CultureInfo toCultureFile,
            string authenticationToken,
            LogEvent logEvent,
            ShallAbort abortRequested)
        {
            var translationResult = new TranslationResult();

            if ((abortRequested != null) && abortRequested())
            {
                return translationResult;
            }

            if (!Directory.Exists(searchRoot))
            {
                return translationResult;
            }

            // Recurse directories
            var subDirs = Directory.GetDirectories(searchRoot);
            foreach (var dir in subDirs)
            {
                translationResult += TranslateResXDirectory(
                    dir,
                    searchFilter,
                    fromCultureBing,
                    toCultureBing,
                    toCultureFile,
                    authenticationToken,
                    logEvent,
                    abortRequested);
                if ((abortRequested != null) && abortRequested())
                {
                    break;
                }
            }

            // Translate resx files
            if ((abortRequested == null) || !abortRequested())
            {
                var files = Directory.GetFiles(searchRoot, searchFilter);
                translationResult = files.Aggregate(
                    translationResult,
                    (current, file) =>
                    current
                    +
                    TranslateResXFile(
                        file,
                        fromCultureBing,
                        toCultureBing,
                        toCultureFile,
                        authenticationToken,
                        logEvent,
                        abortRequested));
            }

            return translationResult;
        }

        /// <summary>Translates resx file</summary>
        /// <param name="fromResxFilePath">The from resx path.</param>
        /// <param name="fromCultureBing">The Source culture in Bing.</param>
        /// <param name="toCultureBing">The Target culture in Bing.</param>
        /// <param name="toCultureFile">The Target culture for the resx file.</param>
        /// <param name="authenticationToken">The service credentials, e.g. bing app id or Azure Account Key.</param>
        /// <param name="logEvent">Event logging delegate</param>
        /// <param name="abortRequested">Event for checking whether abort has been requested</param>
        /// <returns>True if a file has ben translated</returns>
        public static TranslationResult TranslateResXFile(
            string fromResxFilePath,
            CultureInfo fromCultureBing,
            CultureInfo toCultureBing,
            CultureInfo toCultureFile,
            string authenticationToken,
            LogEvent logEvent,
            ShallAbort abortRequested)
        {
            var translationResult = new TranslationResult();

            if ((abortRequested != null) && abortRequested())
            {
                return translationResult;
            }

            if (!File.Exists(fromResxFilePath))
            {
                return translationResult;
            }

            var resXResourceReader = new ResXResourceReader(fromResxFilePath);

            // construct To RESX path
            var directory = Path.GetDirectoryName(fromResxFilePath);
            var resXFromFileExtension = Path.GetExtension(fromResxFilePath);
            var resXRootFileName = Path.GetFileNameWithoutExtension(fromResxFilePath);
            var origLangTag = Path.GetExtension(resXRootFileName);
            CultureInfo origCulture = null;
            try
            {
                if (!string.IsNullOrEmpty(origLangTag))
                {
                    origCulture = CultureInfo.GetCultureInfo(origLangTag.Trim('.'));
                }
            }
            catch (ArgumentException)
            {
            }

            string toResxPath;

            if (origCulture != null)
            {
                toResxPath = Path.ChangeExtension(resXRootFileName, toCultureFile.Name + resXFromFileExtension);
            }
            else
            {
                toResxPath = resXRootFileName + "." + toCultureFile.Name + resXFromFileExtension;
            }

            if (logEvent != null)
            {
                logEvent(toResxPath + " ");
            }

            if (!string.IsNullOrEmpty(directory))
            {
                toResxPath = Path.Combine(directory, toResxPath);
            }

            var stringCount = 00;
            var characterCount = 0;

            // get To Resource Writer
            if (File.Exists(toResxPath))
            {
                try
                {
                    File.Delete(toResxPath);
                }
                catch (IOException e)
                {
                    LogExceptionError(e, logEvent);
                    return translationResult;
                }
                catch (UnauthorizedAccessException e)
                {
                    LogExceptionError(e, logEvent);
                    return translationResult;
                }
            }

            Stream toResxStream = new FileStream(toResxPath, FileMode.CreateNew);
            var toResxWriter = new ResXResourceWriter(toResxStream);
            try
            {
                // get bing reference and response
                var translator = new MicrosoftTranslatorService.LanguageServiceClient();
                var binding = (BasicHttpBinding)translator.Endpoint.Binding; 
                binding.MaxReceivedMessageSize = int.MaxValue;
                binding.MaxBufferSize = int.MaxValue;

                // Set Authorization header before sending the request
                var httpRequestProperty = new HttpRequestMessageProperty { Method = "POST" };
                httpRequestProperty.Headers.Add("Authorization", authenticationToken);

                var options = new MicrosoftTranslatorService.TranslateOptions();
                var resXEntryList = resXResourceReader.Cast<DictionaryEntry>().ToList();
                stringCount = resXEntryList.Count;

                var entriesToTranslate = new List<DictionaryEntry>();
                var requestSize = 0;
                var lastEntryHash = resXEntryList.Last().GetHashCode();
                foreach (var entry in resXEntryList)
                {
                    entriesToTranslate.Add(entry);
                    var valueLength = entry.Value.ToString().Length;
                    requestSize += valueLength;
                    characterCount += valueLength;

                    // The total of all texts to be translated must not exceed 10000 characters. The maximum number of array elements is 2000. 
                    if ((requestSize <= 8000) && (entriesToTranslate.Count < 2000)
                        && (entry.GetHashCode() != lastEntryHash))
                    {
                        continue;
                    }

                    var stringsToTranslate = entriesToTranslate.Select(t => t.Value.ToString()).ToArray();
                    MicrosoftTranslatorService.TranslateArrayResponse[] translatedTexts;
                    try
                    {
                        // Creates a block within which an OperationContext object is in scope.
                        using (new OperationContextScope(translator.InnerChannel))
                        {
                            OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] =
                                httpRequestProperty;
                            translatedTexts = translator.TranslateArray(
                                string.Empty, stringsToTranslate, fromCultureBing.Name, toCultureBing.Name, options);
                        }
                    }
                    catch (Exception e)
                    {
                        LogExceptionError(e, logEvent);
                        return translationResult;
                    }

                    if (logEvent != null)
                    {
                        logEvent(".");
                    }

                    // loop orginal resx and create new one from translated text
                    var l = 0;
                    foreach (var translateEntry in entriesToTranslate)
                    {
                        if ((translatedTexts == null) || (l >= translatedTexts.Length))
                        {
                            if (logEvent != null)
                            {
                                logEvent("inclomplete Translation");
                            }

                            break;
                        }

                        var translatedText = translatedTexts[l].TranslatedText;
                        toResxWriter.AddResource(translateEntry.Key.ToString(), translatedText);
                        l += 1;
                    }

                    Application.DoEvents();

                    if ((abortRequested != null) && abortRequested())
                    {
                        break;
                    }

                    entriesToTranslate.Clear();
                    requestSize = 0;
                }
            }
            finally
            {
                // close RESX
                toResxWriter.Close();
                toResxStream.Close();
                if (logEvent != null)
                {
                    logEvent("\r\n");
                }
            }

            translationResult.FileCount = 1;
            translationResult.StringCount = stringCount;
            translationResult.CharacterCount = characterCount;
            return translationResult;
        }

        /// <summary>
        /// Status logging method
        /// </summary>
        /// <param name="message">The message</param>
        /// <param name="buttons">message buttons to show</param>
        /// <param name="icon">message icon to show</param>
        public static void LogStatus(string message, MessageBoxButtons buttons, MessageBoxIcon icon)
        {
            if (Application.OpenForms.Count > 0)
            {
                // loaded as a wiundows form
                MessageBox.Show(message, Resources.LogStatus, buttons, icon);
            }
            else
            {
                // loaded as a console app
                Console.WriteLine(message);
            }
        }

        /// <summary>
        /// General exception handling
        /// </summary>
        /// <param name="ex">The exception</param>
        /// <param name="logEvent">Loging delegate</param>
        private static void LogExceptionError(Exception ex, LogEvent logEvent)
        {
            if (ex is ArgumentOutOfRangeException)
            {
                if (logEvent != null)
                {
                    logEvent("\r\n");
                    logEvent("Error: " + ex.Message);
                }
            }
            else if (ex is SoapHeaderException)
            {
                if (logEvent != null)
                {
                    logEvent("\r\n");
                    logEvent("Microsoft Translator API Error: " + ex.Message);
                }
            }
            else
            {
                if (logEvent != null)
                {
                    logEvent("\r\n");
                    logEvent("General Error: " + ex.Message);
                }
            }
        }
    }
}
